O que você vai aprender nesta aula?
Após o término da aula você terá aprendido:
Este material usou o Capítulo 1 (Modelo de dados do Python) do livro Python Fluente do Luciano Ramalho
Nesta aula vamos falar sobre como funciona o modelo de dados do Python.
O Python é uma linguagem conhecida por sua consistência. Isso permite que, após trabalhar certo tempo com a linguagem, você consiga ter palpiters corretos sobre recursos do Python que você ainda não domina.
Um exemplo da consistência da linguagem se dá pela função len(), que apesar de parecer estranho de se usar - len(collection) ao invés de collection.len() como é feito em outras linguagens - sabemos, conforme visto no curso, que podemos usá-la para qualquer coleção, enquanto outras linguagens possuem métodos de nomes diferentes para realizar essa mesma operação.
O responsável por consistência (e estranheza) é o Python data model (modelo de dados do Python) que descreve a API que pode ser usada para fazer que seus próprios objetos interajam bem com os recursos mais idiomáticos da linguagem. Ele descreve os objetos e como estes interagem entre si.
O modelo de dados formaliza as interfaces dos blocos de construção da própria linguagem, por exemplo, as sequências, os iteradores, as funções, as classes, os gerenciadores de contexto e assim por diante.
O python faz isso usando os métodos especiais: o interpretador do Python chama esses métodos para realizar operações básicas em objetos, geralmente acionados por uma sintaxe especial.
Os métodos especiais são sempre escritos com underscores duplos no início e no fim (como __getitem__). Por exemplo a sintaxe especial obj[chave] é tratada pelo método especial __getitem__. Quando o interpretador avalia colecao[chave] ele chama colecao.__getitem__(chave).
Vamos mostrar um exemplo de como podemos usar o modelo de dados do python a nosso favor. Vamos criar um baralho pythônico:
In [1]:
from exemplos.baralho import Baralho
baralho = Baralho()
Podemos acessar as cartas do baralho por índice:
In [2]:
baralho[0]
Out[2]:
In [3]:
baralho[-1]
Out[3]:
Também podemos realizar slicing no baralho:
In [4]:
baralho[:5]
Out[4]:
In [5]:
baralho[15:20]
Out[5]:
In [6]:
baralho[-5:]
Out[6]:
E iterá-lo:
In [7]:
for carta in baralho:
print(carta)
Iterá-lo de trás para frente:
In [8]:
for carta in reversed(baralho):
print(carta)
Enumerá-lo!!!111!!!onze!!11!
In [9]:
for carta in enumerate(baralho):
print(carta)
Sorteio de cartas usando o módulo random:
In [10]:
from random import choice
choice(baralho)
Out[10]:
In [11]:
choice(baralho)
Out[11]:
In [12]:
choice(baralho)
Out[12]:
Sorteando 5 cartas (pode haver repetição):
In [13]:
mao = [choice(baralho) for _ in range(5)]
mao
Out[13]:
Também podemos verificar se uma carta específica está no baralho:
In [14]:
from exemplos.baralho import Carta
Carta('10', 'espadas') in baralho
Out[14]:
In [15]:
Carta('3', 'alabardas') in baralho
Out[15]:
E se saber quantas cartas há no baralho:
In [16]:
len(baralho)
Out[16]:
Você deve estar se perguntando quanto custou para implementar tudo isso? Respota: muito pouco.
""" Arquivo: 02-python-oo/aula-03/exemplos/baralho.py """
from collections import namedtuple
Carta = namedtuple('Carta', ['valor', 'naipe'])
class Baralho:
valores = [str(n) for n in range(2, 11)] + list('AJQK')
naipes = 'copas ouros paus espadas'.split()
def __init__(self):
self.cartas = [Carta(v, n) for v in self.valores for n in self.naipes]
def __len__(self):
return len(self.cartas)
def __getitem__(self, pos):
return self.cartas[pos]
Vimos duas vantagens de usar os métodos especiais para tirar proveito do modelo de dados do Python:
Os usuarios de suas classes não precisarão memorizar nomes arbitrários de métodos para realizar operações comuns (Como
obter a quantidade de itens? Uso .size(), .length(), ou o quê?)
Podemos se beneficiar da biblioteca-padrão do Python e não reinventar a roda, como visto no uso das funções random.choice e reversed.
Os métodos especiais foram criados para serem chamados pelo interpretador Python e não diretamente. Não usamos objeto.__len__, para obter a quantidade de elementos, mas sim len(objeto). Se objeto for a instância de uma classe definida pelo usuário (programador), o Python chamará o método __len__ da instância.
Na grande maioria das vezes a chamada aos métodos especiais será feita de forma implícita. Por exemplo, a construção de for i in x invoca iter(x), que poderá chamar x.__iter__() se existir.
Um exemplo comum de implementação e chamada de métodos especiais diretamente é o __init__ para sobrescrever o inicilizador da superclasse. Também é comum invocar o inicializador da superclasse diretamente com, por exemplo, super().__init__() ao implementar seu próprio inicializador.
Caso precise chamar um método especial, em geral é muito melhor chamar a função embutida relacionada ou a sintaxe especial (obj[chave], len, iter, str etc.). Essas funções embutidas invocam o método especiail correspondente, porém, com frequência, oferecem outros serviços e - para os tipos embutidos - são mais rápidas que chamadas de métodos.
Nós podemos fazer todas essas operações no Baralho sem herdar de alguma classe espeicial, pois implementamos o protocolo de sequência como definido no modelo de dados do Python. Agora ficam duas dúvidas: o que é exatamente um protocolo e uma sequência?
No contexto de programação orientada a objetos um protocolo é uma interface informal definida somente na documentação e não no código. Por exemplo, o protocolo de sequência em Python implica somente os métodos __len__ e __getitem__. Qualquer classe que implemente esses métodos poderá ser usada em qualquer lugar em que se espera uma sequência.
Esse tipo de programação ficou conhecida como Duck Typing e é muito comum em linguagens dinâmicas como Python e Ruby.
"Não verifique se é um pato: verifique se faz quack como um pato, anda como um pato etc., de acordo com o subconjunto exatao de comportamento de pato de que você precisa para usar a linguagem." (Alex Martelli, 2000)
Essa técnica consiste em não verificar se uma classe é, por exemplo, uma sequência e sim se ela se comporta como uma sequência.
É importante notar que, como os protocolos são informais e não impostos. Geralmente você pode implementar somente a parte de um protocolo que faz sentido a sua aplicação sem que haja problemas. Por exemplo, para dar suporte a iteração é necessário implementar somente o método __getitem__ e não é necessário o __len__
Agora que sabemos como funcionam os protocolos em Python, vamos falar sobre o protocolo de sequência.
O python data model define sequências como conjuntos finitos indexados por números não negativos. Sendo n o tamanho da sequência, os índices vão de 0 a n - 1 e são acessados por a[i].
Falaremos mais sobre o protocolo de sequência futuramente. Caso queira entender mais sobre o assunto consulte sua documentação.
Vamos ver como utilizar os métodos especiais para emular tipos numéricos.
O python data model diz que números são criados por números declará-los em sua forma literal (como por exemplo a = 3, 3.4 etc.) e resultados de operações aritméticos e funções aritméticas embutidas.
Implementaremos uma classe para representar vetores bidimensionais (vetores euclidianos) usados na matemática e na física.
In [17]:
from exemplos.vetor1 import Vetor
v1 = Vetor(1, -2)
v2 = Vetor(3, 4)
Podemos somar vetores usando o operador +:
In [18]:
v1 + v2
Out[18]:
Usar o operador de subtração:
In [19]:
v1 - v2
Out[19]:
Multiplicação por escalar:
In [20]:
v1 * 3
Out[20]:
In [21]:
v2 * -4
Out[21]:
Valor absoluto (distância do vetor até a origem):
In [22]:
abs(v2)
Out[22]:
Comparação de vetores por valor:
In [23]:
v1 == v2
Out[23]:
In [24]:
v1 == Vetor(1, -2)
Out[24]:
In [25]:
v2 == Vetor(3, 4)
Out[25]:
Podemos fazer verificações booleanas com o vetor:
In [26]:
if v1:
print('v1 existe e possui valor')
In [27]:
if not Vetor(0, 0):
print('vetor não possui valor')
else:
print('alguma coisa deu errado')
Esse exemplo usou a classe Vetor demonstrada a seguir, que implementa as operações demonstradas por meio dos métodos especiais __repr__, __abs__, __add__, __bool__, __eq__, __sub__ e __mul__:
"""
Arquivo: 02-python-oo/aula-03/exemplos/vetor.py
Implementa um vetor bidimensional
"""
import math
class Vetor:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
return 'Vetor({!r}, {!r})'.format(self.x, self.y)
def __abs__(self):
return math.hypot(self.x, self.y)
def __add__(self, v2):
return Vetor(self.x + v2.x, self.y + v2.y)
def __bool__(self):
return bool(self.x or self.y)
def __eq__(self, v2):
return self.x == v2.x and self.y == v2.y
def __sub__(self, v2):
return Vetor(self.x - v2.x, self.y - v2.y)
def __mul__(self, scalar):
return Vetor(self.x * scalar, self.y * scalar)
O método __repr__ é responsável por retornar a representação do objeto para inspeção. Esse valor é usado no modo interativo e em debugers. Caso esse método não seja sobrescrito será exibido algo como <Vetor object at 0x123e9230>.
A representação do objeto é obtido a partir da função embutida repr(). É uma boa prática usar !r para obter a representação dos atributos do objeto, pois mostra a diferença fundamental entre Vector(1, 2) e Vector('1', '2') - a última não funcionará, pois os argumentos do construtor devem ser número e não str.
Também há o método __str__ que é utilizado para exibir o valor do objeto para o usuário final. Para entender melhor a diferença consulte esta thread do stack overflow que foi muito bem respondida pelos pythonistas Alex Martelli e Martijn Peters.
Esse exemplo contém alguns problemas:
In [28]:
v1
Out[28]:
In [29]:
v1 * 5
Out[29]:
In [30]:
5 * v1
No exemplo anterior tentamos multiplicar um int por um Vetor, porém foi levantada uma exceção já que o tipo int não sabe multiplicar por Vetor. Apenas Vetor sabe multiplicar por escalar.
Para resolver esse problema precisamos antes entender como funciona x * y:
x tiver x.__mul__, chama x.__mul__(y) e devolve o resultado a menos que seja NotImplementedx não tiver x.__mul__, ou sua chamada devolver NotImplemented, verifica se y possui __rmul__, chama y.__rmul__(x) e devolve o resultado, a menos que seja NotImplementedy não tiver __rmul__, ou sua chamada devolver NotImplented, levanta TypeError com uma mensagem unsupported operand type(s)O método __rmul__ é chamado de versão refletida, reversa ou direita (do inglês right) de __mul__.
Para corrigir precisamos adicionar o método __rmul__ à clase Vetor:
import math
class Vetor:
...
def __mul__(self, escalar):
return Vetor(self.x * escalar, self.y * escalar)
def __rmul__(self, outro):
return self * outro
Porém, ao adicionar esse código acontece outro problema:
In [33]:
from exemplos.vetor2 import Vetor
Vetor(1, 2) * Vetor(2, 4)
Esse resultado não faz sentido, não é assim que multiplicação de vetores funciona.
Não vamos implementar aqui a multiplicação de vetores, pois o foco da aula é ensinar programação e não matemática. Portanto, precisamos permitir a multiplicação de vetores apenas por escalares, vamos corrigir a função __mul__:
import math
from numbers import Number
class Vetor:
...
def __mul__(self, escalar):
if isinstance(escalar, Real):
return Vetor(self.x * escalar, self.y * escalar)
else:
return NotImplemented
Verificamos se o escalar recebido de fato é um número real, se for retornamos o resultado da multiplicação do vetor pelo escalar, caso contrário é retornado NotImplemented.
Retornamos NotImplemented ao invés de levantar uma exceção, para permitir que o Python tente executar __rmul__ no escalar, pois pode ser que seja algum tipo que implemente a operação reversa da multiplicação.
Também há um problema com a comparação de valores quando comparamos vetores com outros tipos:
In [34]:
from exemplos.vetor1 import Vetor
Vetor(1, 3) == [1, 2]
In [35]:
Vetor(2, 4) == 'oi'
Essa comparação deveria retornar False, não levantar uma exceção. Podemos corrigir esse problema da seguinte maneira:
import math
from numbers import Number
class Vetor:
...
def __eq__(self, outro):
if isinstance(outro, Vetor):
return self.x == outro.x and self.y == outro.y
else:
return NotImplemented
Agora podemos comparar Vetor com outros tipos:
In [2]:
from exemplos.vetor2 import Vetor
Vetor(1, 3) == 8
Out[2]:
In [3]:
Vetor(-2, 3) == [1, 2, 3]
Out[3]:
Nosso vetor ainda não suporta operações unárias como -v e +v:
In [10]:
from exemplos.vetor1 import Vetor
-Vetor(1, 5)
In [11]:
+Vetor(2, 3)
Para que esses operadores funcionem precisamos definir os métodos __neg__ e __pos__:
from numbers import Real
import math
class Vetor:
...
def __neg__(self):
return self * -1
def __pos__(self):
return self
A função __neg__ simplesmente retornou o vetor por -1. Já a função __pos__ retorna a própria instância, pois +Vetor(x, y) é sempre igual a ele mesmo Vetor(x, y).
Seria interessante se pudessemos desempacotar os valores de x e de y de um vetor para uma tupla. Isso facilitaria nossa vida, pois poderiamos fazer isso:
In [13]:
v = Vetor(3, -1)
In [16]:
x, y = v
x, y
Ao invés disso:
In [17]:
x = v.x
y = v.y
x, y
Out[17]:
O desempacotamento facilita ainda mais nossa vida se tivessemos uma lista de vetores:
In [20]:
from random import randint
lista_vetores = [Vetor(x=randint(-10, 10), y=randint(-10, 10)) for _ in range(5)]
lista_vetores
Out[20]:
Pois poderiamos ter acesso facilitado a x e y durante uma iteração usando o desempacotamento de sequências:
In [21]:
for x, y in lista_vetores:
print(x, y)
Ao invés de ter que acessar os atributos diretamente:
In [23]:
for vetor in lista_vetores:
print(vetor.x, vetor.y)
Antes de implementar essa funcionalidade precisamos entender como funciona o desempacotamento de sequências: o objeto a direita é iterado e cada variável a esquerda é atribuída ao item resultante dessa iteração.
In [30]:
(a, b) = (1, 0)
a, b
Out[30]:
In [27]:
[a, b] = [3, 4]
a, b
Out[27]:
O que acontece por trás de tudo isso é: Extraímos o iterador da sequência a direita:
In [33]:
iterador = iter([3, 4])
Ele é iterado uma vez e o resultado da iteração é atribuído a primeira variável:
In [ ]:
a = next(iterador)
E é iterado até chegar ao último elemento:
In [34]:
b = next(iterador)
In [35]:
a, b
Out[35]:
Agora que sabemos disso fica claro que, para nosso vetor ser desempacotado precisamos torná-lo iterável. Para isso podemos definir o método __iter__ que deve retornar um iterador:
...
class Vetor:
...
def __iter__(self):
return iter((self.x, self.y))
Nesse método definimos uma tupla composta pelos atributos x e y da instância do Vetor e retornamos o iterador da dessa tupla.
Essa implementação funciona, porém podemos usar geradores para deixar esse método mais simples e eficiente:
...
class Vetor:
...
def __iter__(self):
yield self.x; yield self.y
Para entender essa implementação é necessário conhecer o funcionamento de geradores, que veremos numa aula futura.
As operações que implementamos para nosso vetor não o alteram, mesmo quando usamos operadores acumulados:
In [36]:
from exemplos.vetor2 import Vetor
v = Vetor(1, 2)
v, id(v)
Out[36]:
Se realizarmos uma soma acumulada com outro vetor:
In [37]:
v += Vetor(2, 3)
v, id(v)
Out[37]:
Um novo objeto é criado, pois o objeto referenciado pela variável v não é mais o mesmo (a identidade dos objetos são diferentes).
Para que essas operações de fato modifiquem um objeto, como acontecem com objetos mutáveis:
In [38]:
lista = [1, 2, 3, 4]
lista, id(lista)
Out[38]:
In [39]:
lista += [5, 6, 7, 8]
lista, id(lista)
Out[39]:
O objeto permanece o mesmo, seu valor que é alterado.
Matemáticamente não faz muito sentido ter um vetor mutável, mas para entendermos melhor esses conceitos vamos fazer um VetorMutável, como subclasse de Vetor, que altere o valor do vetor quanto as operações += e *= forem usadas implementando os métodos __iadd__ e __imul__:
In [42]:
from exemplos.vetor2 import VetorMutavel
vm = VetorMutavel(2, 3)
vm, id(vm)
Out[42]:
In [43]:
vm += VetorMutavel(-1, 4)
vm, id(vm)
Out[43]:
In [44]:
vm *= -2
vm, id(vm)
Out[44]:
Essa classe VetorMutavel pode ser implementada da seguinte maneira:
class VetorMutavel(Vetor):
def __iadd__(self, outro):
if isinstance(outro, Vetor):
self.x += outro.x
self.y += outro.y
return self
return NotImplemented
def __imul__(self, outro):
if isinstance(outro, Real):
self.x *= outro
self.y *= outro
return self
return NotImplemented
In [23]:
def chamavel():
print('posso ser chamado')
In [2]:
chamavel()
In [22]:
type(chamavel)
Out[22]:
In [3]:
class Foo:
def bar(self):
print('também posso ser chamado!')
Quando classes são chamadas retornam instâncias:
In [6]:
foo = Foo()
In [7]:
foo.bar()
In [8]:
def gen():
yield 1
Chamar geradores retorna objetos geradores que executam o código definido
In [11]:
gen()
Out[11]:
Para acessar o conteúdo do gerador precisamos iterá-lo, para isso podemos usar a função embutida next() :
In [21]:
g = gen()
next(g)
Out[21]:
Porém se requisitamos mais valores de um gerador que ele pode gerar uma exceção é levantada:
In [20]:
next(g)
Veremos mais sobre geradores nas próximas aulas. Para saber mais sobre chamáveis consulte o python data model e como chamáveis são expressados
Por fim, objetos que definem um método __call__ também são chamáveis.
Para demonstrar isso vamos implementar uma tombola (gaiola de bingo). A tombola pode:
Vamos ao código:
In [79]:
import random
class Tombola:
def __init__(self, itens=None):
self._itens = []
self.carrega(itens)
def __call__(self):
return self.sorteia()
def carrega(self, itens):
self._itens.extend(itens)
def inspeciona(self):
return tuple(self._itens)
def mistura(self):
random.shuffle(self._itens)
def sorteia(self):
return self._itens.pop()
def vazia(self):
return len(self._itens) == 0
Vamos criar nossa tombola que armazena os números de 1 a 20:
In [26]:
tombola = Tombola(range(1, 21))
Verificaremos seus itens:
In [27]:
tombola.inspeciona()
Out[27]:
Está vazia?
In [28]:
tombola.vazia()
Out[28]:
Misturando:
In [29]:
tombola.mistura()
tombola.inspeciona()
Out[29]:
Sorteando um item da maneira clássica:
In [30]:
tombola.sorteia()
Out[30]:
Aproveitando o método __call__ que definimos podemos sortear chamado o objeto tombola sem chamar o método tombola.sorteia():
In [31]:
tombola()
Out[31]:
Os operadores "aritméticos" (como +, * etc.) também podem ser aplicados a outros objetos para realizar operações que fazem sentido a esse objetos. Como por exemplo em listas e strings, em que o operador + realiza a concatenação:
In [32]:
lista = [1, 2, 3, 4]
lista
Out[32]:
In [33]:
lista + [5, 6, 7, 8]
Out[33]:
In [34]:
[-4, -3, -2, -1, 0] + lista
Out[34]:
In [35]:
pal = "palavra"
pal
Out[35]:
In [37]:
pal + '!!!1!1!11onze!!!1!'
Out[37]:
Para mostrar como isso funciona vamos implementar uma tombola expansível que torne possível juntar os itens dessa tombola com outra tombola ou um iterável.
Vamos definir o método __add__ para permitir a "soma" de tombolas:
In [39]:
class TombolaExpansivel(Tombola):
def __add__(self, other):
if isinstance(other, Tombola):
return TombolaExpansivel(self.inspeciona() + other.inspeciona())
else:
return NotImplemented
Na linha 3 verificamos se o objeto somado é uma instância de Tombola, isso permite que nossa TombolaExpansivel seja somada com Tombola e todas suas subclasses:
In [62]:
tombola_exp = TombolaExpansivel(range(1, 11))
tombola_exp.inspeciona()
Out[62]:
Podemos somar a instância de TombolaExpansivel com Tombola e suas subclasses:
In [44]:
outra_tombola = tombola_exp + Tombola(range(11, 21))
outra_tombola.inspeciona()
Out[44]:
In [57]:
mais_tombolas = tombola_exp + TombolaExpansivel(range(11, 16))
mais_tombolas.inspeciona()
Out[57]:
Sobrescrevendo o método __add__ já é possível usar a soma atribuída, porém haverá um problema indesejado:
In [58]:
id(tombola_exp), tombola_exp.inspeciona()
Out[58]:
In [63]:
tombola_exp += Tombola(range(11, 16))
id(tombola_exp), tombola_exp.inspeciona()
Out[63]:
Vemos que as identidades dos objetos atribuidos a tombola_exp são diferentes. Isso por que a atribuição acumulada, por padrão, na verdade faz:
In [64]:
tombola_exp = tombola_exp + Tombola(range(16, 21))
In [65]:
tombola_exp.inspeciona()
Out[65]:
E nossa função o __add__ cria um novo objeto. Como queremos que nossa TombolaExpansivel seja mutável, precisamos definir o método __iadd__ para modificar a instância.
Aproveitando que vamos mexer no __iadd__ podemos melhorar nossa tombola para receber item de qualquer iterável e não somente de Tombola e suas subclasses:
In [80]:
class TombolaExpansivel(Tombola):
def __add__(self, other):
if isinstance(other, Tombola):
return TombolaExpansivel(self.inspeciona() + other.inspeciona())
else:
return NotImplemented
def __iadd__(self, outro):
if isinstance(outro, Tombola):
outro_iteravel = outro.inspeciona()
else:
try:
outro_iteravel = iter(outro)
except TypeError:
msg = "operando da direita no += deve ser {!r} ou um iterável"
raise TypeError(msg.format(type(self).__name__))
self.carrega(outro_iteravel)
return self
Linha 9 e 10: se o objeto à direita for uma tombola inspecionamos e "pegamos" seus itens
Linha 12 e 13: tenta extrair um iterável do objeto a direita, isso funcionará se este objeto for iterável, se não uma exceção do tipo TypeError é levantada.
Linha 14, 15 e 16: Se for levantada uma exceção TypeError é criada uma outra exceção do tipo TypeError, porém com uma mensagem de erro mais clara.
Linha 17: carrega os próprios itens e da outra tupla.
Agora podemos, de fato, modificar nossa TombolaExpansível:
In [81]:
tombola_exp = TombolaExpansivel(range(-10, 1, 1))
id(tombola_exp), tombola_exp.inspeciona()
Out[81]:
In [82]:
tombola_exp += [1, 2, 3, 4]
id(tombola_exp), tombola_exp.inspeciona()
Out[82]:
Para finalizar, vamos ver uma tabela todos os métodos especiais do Python. Algum dos métodos especiais ainda não foram explicados no curso e outros não serão, portanto consulte a documentação caso você precise deles.
Esta tabela foi tirada do livro Fluent Python (Python Fluente)
Na tabela a seguir constam todos os métodos mágicos por tipo. (em inglês, pois não há ebook da versão pt-br)
Fim da aula 03